Repository navigation
fix: keep RSA-OAEP-256 digest correct in runtimes that ignore oaepHash - #106
mmorales-post wants to merge 3 commits into
Conversation
f19a834 to
ccb1501
Compare
talha-API
left a comment
There was a problem hiding this comment.
Thanks for raising this PR and issue @mmorales-post and apologies for the delay in the response.
The changes are sound, just a few points to address. Please also update the PR with latest changes from main (if not done already) and add any relevant tests for the comments provided.
| if ( | ||
| typeof nodeCrypto.publicEncrypt !== "function" || | ||
| !nodeCrypto.constants || | ||
| !keyPem |
There was a problem hiding this comment.
honoursOaepHash will cache false when the key is missing. The key is not related to the runtime environment.
Also, an encrypt-only instance that calls decryptData sets the module-level nativeOaepHash to false. Every JweCrypto in the process then falls back to node-forge, even on a real Node runtime, which should not be the case.
There was a problem hiding this comment.
Fixed. The probe now always uses the encryption certificate, so a missing private key can't affect it.
| }); | ||
| } | ||
| }); | ||
|
|
There was a problem hiding this comment.
Please add a test covering the forge path for decryptData.
There was a problem hiding this comment.
Added tests for forge decrypt: tokens wrapped by node and by forge, plus a SHA-1 wrapped key that gets rejected.
| Buffer.alloc(1) | ||
| ); | ||
| nativeOaepHash = false; | ||
| } catch { |
There was a problem hiding this comment.
This could catch unrelated exceptions, we should probably check for a digest specific one.
There was a problem hiding this comment.
Fixed. It now only treats node's invalid-digest error as support. Anything else isn't cached and falls back to forge.
talha-API
left a comment
There was a problem hiding this comment.
Thanks for addressing the comments @mmorales-post , please see additional minor comments and I request you to provide some further context on the points below:
- Which runtime did you run into this issue in? Since the issue specified
crypto-browserifyandpublic-encrypt, I assume a browser bundle. If not, please share the runtime and the bundler/versions. - As you noted in the scope section, this change targets only the digest issue. A client must still polyfill
process.version(≥ 13),fs,Buffer,cryptoetc. How does your setup provide them? - I have tested the changes and they do not introduce any regressions, but please share evidence from the setup where you found this (browser/postman/insomnia, error logs etc) It will help us review and assess the need for JWE support outside native Node.
| // Accepted a digest that does not exist, so it is not using the option. | ||
| nativeOaepHash = false; | ||
| } catch (err) { | ||
| if (!err || err.code !== "ERR_OSSL_EVP_INVALID_DIGEST") { |
There was a problem hiding this comment.
Could you confirm this is the same exception thrown for all node versions supported for JWE flow? i.e. Node >v12
There was a problem hiding this comment.
Yes. I ran the probe against a real certificate on every major from 13 to 24 (node:<v>-alpine), plus 26 locally. All of them throw ERR_OSSL_EVP_INVALID_DIGEST ("Invalid digest used"), on OpenSSL 1.1.1 (13 to 16) and 3.x (17 on). 13 is the floor because nodeVersionSupportsJWE requires > 12. I've added the range to the doc comment.
v13.14.0 openssl=1.1.1g code=ERR_OSSL_EVP_INVALID_DIGEST
v14.21.3 openssl=1.1.1t code=ERR_OSSL_EVP_INVALID_DIGEST
v15.14.0 openssl=1.1.1k+quic code=ERR_OSSL_EVP_INVALID_DIGEST
v16.20.2 openssl=1.1.1v+quic code=ERR_OSSL_EVP_INVALID_DIGEST
v17.9.1 openssl=3.0.3+quic code=ERR_OSSL_EVP_INVALID_DIGEST
v18.20.8 openssl=3.0.16 code=ERR_OSSL_EVP_INVALID_DIGEST
v19.9.0 openssl=3.0.8+quic code=ERR_OSSL_EVP_INVALID_DIGEST
v20.20.2 openssl=3.0.19 code=ERR_OSSL_EVP_INVALID_DIGEST
v22.23.4 openssl=3.5.9 code=ERR_OSSL_EVP_INVALID_DIGEST
v24.22.2 openssl=3.5.9 code=ERR_OSSL_EVP_INVALID_DIGEST
If a future release changes the code, the probe fails safe: nothing is cached and that call goes through forge.
| * Build node-forge OAEP options for RSA-OAEP-256. | ||
| * | ||
| * The JWE header emitted by this module is always `"alg": "RSA-OAEP-256"`, so | ||
| * the label digest and the MGF1 digest are both SHA-256. MGF1 has to be set |
There was a problem hiding this comment.
Since the library specifies "node-forge": "^1.4.0", the claim “MGF1 has to be set explicitly or forge defaults it to SHA-1” is not entirely accurate. The thing that defaults to SHA-1 is the OAEP digest, md. MGF1 follows whatever md is.
But since explicitly setting mgf1: { md } is harmless, mirrors what FieldLevelCrypto is doing and clarifies intent, we can keep it. Please just update the comment to reflect actual behaviour.
There was a problem hiding this comment.
You're right. In forge 1.4.0 MGF1 falls back to md (pkcs1.js), and only md itself defaults to SHA-1. I've updated the comment to say that, and fixed the same claim in the PR description.
|
Thanks @talha-API. Comment fixes are pushed. On your three questions: 1. Runtime. The Postman script runtime, which runs the same way in the Postman app, the Postman CLI and Postman monitors. It isn't a browser, but it has the same constraints: no The bundle I validated with: webpack 5.112.2, 2. The other polyfills.
None of that needs a change to this library. The digest is the only part a polyfill can't fix: the polyfill accepts 3. Evidence. a) The same bundle built from On b) I also confirmed it end to end against Mastercard's sandbox environment: the SHA-1-wrapped token from c) In the same bundle, in the same Postman script runtime, the field-level path is already correct, because On the need for JWE outside native node: |
PR checklist
masterbranchLink to issue/feature request: #105
Description
The JWE key wrap asked node's
crypto.publicEncryptforoaepHash: "sha256". nodehonours that; the
crypto-browserifypolyfill silently ignores it and wraps with SHA-1,producing a token whose header claims
RSA-OAEP-256. It fails only at the recipient.node callers are unaffected. The native path is retained and selected by a one-time
probe. Measured 0.055 ms against 0.050 ms on
mainforencryptData, within noise.The probe. node validates the digest name and throws on an unrecognised one; a
polyfill that ignores
oaepHashaccepts anything. Attempting a wrap under a deliberatelybogus digest therefore distinguishes them, costs one call, and is memoised. It also treats
an absent or stubbed
cryptomodule as unsupported, which is whatresolve.fallback.crypto = falsein this repo'swebpack.config.jsproduces.The fallback goes through node-forge with SHA-256 set for the OAEP digest, which
forge otherwise defaults to SHA-1. MGF1 follows that digest when unset; it is set
explicitly anyway to make the intent clear. The helper mirrors
createOAEPOptionsin
field-level-crypto.js. Forge keys are parsed lazily, so node callers never parse them.decryptDatagets the same treatment, so a browser build can read responses as well assend requests.
Scope. This fixes the digest, which is the failure that is silent. It does not make
the library work under
resolve.fallback.crypto = false, becauserandomBytes,createCipherivandcreateDecipherivstill require the module. Under a bundler thatpolyfills
cryptothe full JWE path now works. Happy to raise the remaining pieceseparately if that is useful.
Validation
Three tests added to
test/jwe-crypto.test.js:still wraps with SHA-256 when the runtime ignores oaepHashstubspublicEncrypttobehave like the polyfill, then asserts the emitted key unwraps with SHA-256 and not
SHA-1. Fails on
main.detects whether the runtime honours oaepHashcovers the probe against real node, apolyfill, and an empty module. Fails on
main.wraps the content encryption key with RSA-OAEP-256, not SHA-1locks the digest for thenative path. This one passes on
mainas well, by design: under node the old code wasalready correct, which is why the bug survived. It is there to stop a regression.
Cross-checked that forge and native interoperate in both directions on the same key, and
that a SHA-1 unwrap of the forge output is rejected.
Full suite: 200 passing. eslint: 0 errors, 5 warnings, unchanged from
main.Validated on Node 17, 22 and 24. Probe error code verified on node 13 through 24 and 26.